package it.unibo.refolding.pico.mfp;

import it.unibo.refolding.common.CommonThings;
import it.unimi.dsi.fastutil.bytes.ByteArrayList;
import it.unimi.dsi.fastutil.bytes.ByteList;
import it.unimi.dsi.fastutil.bytes.ByteLists;

import java.nio.ByteBuffer;
import java.nio.ShortBuffer;
import java.util.Arrays;
import java.util.Collections;
import java.util.Locale;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;

public class Waves {
  public static Wave readWave(ByteBuffer bb) {
    ByteOrderAndVersion bov = ByteOrderAndVersion.read(bb);
    bb.order(bov.byteOrder);
    switch (bov.version) {
    case 2:
      return readWave2(bov, bb);
    case 5:
      return readWave5(bov, bb);
    default:
      throw new IllegalArgumentException(String.format(
        Locale.ROOT, "Version %s is not supported.", bov.version));
    }
  }

  public static Wave readWave2(ByteOrderAndVersion bov, ByteBuffer bb) {
    Checksummed<BinHeader2> checksummedBinHeader2 = BinHeader2.read(bb);
    Checksummed<WaveHeader2> checksummedWaveHeader2 = WaveHeader2.read(bb);
    BinHeader2 binHeader2 = checksummedBinHeader2.object;
    int waveDataSize = waveDataSize(binHeader2);
    WaveHeader2 waveHeader2 = checksummedWaveHeader2.object;
    int numBytesPerPoint = IgorUtil.numBytesPerPoint(waveHeader2.type);
    int numBytesToRead = waveHeader2.npnts * numBytesPerPoint;
    // TODO: What happens if NT_I8 and npnts is odd?
    Preconditions.checkArgument(
      waveDataSize == numBytesToRead, "We don't support formula or text modes: %s != %s.",
      waveDataSize, numBytesToRead);
    byte[] waveData = new byte[numBytesToRead];
    ByteBuffer slice = bb.slice();
    // The byte order is not inherited.
    slice.order(bb.order());
    bb.get(waveData);
    ShortBuffer sb = slice.asShortBuffer();
    int first16UnsignedChecksum = 0;
    for (int i = 0; i < 8; ++i) {
      first16UnsignedChecksum = IgorUtil.addShort(sb.get(), first16UnsignedChecksum);
    }
    short checksum = Checksummed.checksum(
      bov.unsignedChecksum(), checksummedBinHeader2.unsignedChecksum,
      checksummedWaveHeader2.unsignedChecksum, (short) first16UnsignedChecksum);
    Preconditions.checkArgument(checksum == 0, "Invalid file: checksum=%s.", checksum);
    // Read and ignore a padding of 16 bytes.
    byte[] pad = new byte[16];
    bb.get(pad);
    Preconditions.checkState(!CommonThings.any(pad));
    // The note field is *not* null-terminated!
    byte[] note = new byte[binHeader2.noteSize];
    bb.get(note);
    Preconditions.checkArgument(!bb.hasRemaining());
    return new Wave(
      bov, waveHeader2, ByteLists.unmodifiable(ByteArrayList.wrap(waveData)),
      ByteLists.unmodifiable(ByteArrayList.wrap(note)),
      ImmutableList.<ByteList>of());
  }
  
  public static Wave readWave5(ByteOrderAndVersion bov, ByteBuffer bb) {
    Checksummed<BinHeader5> checksummedBinHeader5 = BinHeader5.read(bb);
    Checksummed<WaveHeader5> checksummedWaveHeader5 = WaveHeader5.read(bb);
    short checksum = Checksummed.checksum(
      bov.unsignedChecksum(), checksummedBinHeader5.unsignedChecksum,
      checksummedWaveHeader5.unsignedChecksum);
    Preconditions.checkArgument(checksum == 0, "Invalid file: checksum=%s.", checksum);
    BinHeader5 binHeader5 = checksummedBinHeader5.object;
    int waveDataSize = waveDataSize(binHeader5);
    WaveHeader5 waveHeader5 = checksummedWaveHeader5.object;
    int numBytesPerPoint = IgorUtil.numBytesPerPoint(waveHeader5.type);
    int numBytesToRead = waveHeader5.npnts * numBytesPerPoint;
    // TODO: What happens if NT_I8 and npnts is odd?
    Preconditions.checkArgument(
      waveDataSize == numBytesToRead, "We don't support formula or text modes: %s != %s.",
      waveDataSize, numBytesToRead);
    byte[] waveData = new byte[numBytesToRead];
    bb.get(waveData);
    // The note field is *not* null-terminated!
    byte[] note = new byte[binHeader5.noteSize];
    bb.get(note);
    ByteList[] unsignedLabels = new ByteList[binHeader5.dimLabelsSize.size()];
    for (int i = 0; i < binHeader5.dimLabelsSize.size(); ++i) {
      int labelSize = binHeader5.dimLabelsSize.getInt(i);
      byte[] label = new byte[labelSize];
      bb.get(label);
      unsignedLabels[i] = ByteLists.unmodifiable(ByteArrayList.wrap(label));
    }
    Preconditions.checkArgument(!bb.hasRemaining());
    return new Wave(
      bov, waveHeader5, ByteLists.unmodifiable(ByteArrayList.wrap(waveData)),
      ByteLists.unmodifiable(ByteArrayList.wrap(note)),
      Collections.unmodifiableList(Arrays.asList(unsignedLabels)));
  }
  
  static int waveDataSize(BinHeader2 binHeader2) {
    return binHeader2.wfmSize - WaveHeader2.byteCount() - 16;
  }

  static int waveDataSize(BinHeader5 binHeader5) {
    return binHeader5.wfmSize - WaveHeader5.byteCount();
  }
}
